package alloy.viz.alloyviz;

import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import alloy.viz.dotviz.DotColor;
import alloy.viz.dotviz.DotOrientation;
import alloy.viz.dotviz.DotPalette;
import alloy.viz.dotviz.DotShape;
import alloy.viz.dotviz.DotStyle;

import nanoxml.XMLElement;

public final class ViewPaletteXML {

    /** Returns null if the element is malformed. */
    public static ViewPalette readViewPalette(XMLElement x) {
        /* class ViewPalette {
         *  String name;
         *  List<View> views;
         *  View currentView;
         * }
         * =>
         * <palette name="name">
         *   0 or more <view>...</view>, where at most one of them will say current="yes"
         *   <view>...</view>
         * </palette>
         */
        if (!"palette".equals(x.getName())) return null;
        String name=(String)(x.getAttribute("name"));
        ViewPalette ans=new ViewPalette(name==null ? "" : name);
        Set names=new LinkedHashSet();
        Vector subs=x.getChildren();
        for(int i=0; i<subs.size(); i++) {
            XMLElement xml=(XMLElement)(subs.get(i));
            if (!"view".equals(xml.getName())) continue;
            View view=parseView(xml);
            if (view==null) continue;
            if (names.contains(view.getName())) continue;
            ans.addView(view);
            Boolean current=getbool(xml, "current", null);
            if (current!=null && current.booleanValue()) ans.setCurrentView(view);
        }
        return ans;
    }

    /** Writes nothing if the argument is null. */
    public static void writeViewPalette(Writer out, ViewPalette x) throws IOException {
        if (x==null) return;
        out.write("<?xml version=\"1.0\"?>\n\n");
        out.write("<palette name=\""); encode(out, x.getName()); out.write("\">\n");
        View currentview=x.getCurrentView();
        for(Iterator i=x.getAllViews().iterator(); i.hasNext();) {
            out.write('\n');
            View view=(View)(i.next());
            writeView(out, view, view==currentview);
        }
        out.write("\n</palette>\n");
    }

    /* ******************************************************************************************* */

    /** Returns null if the element is malformed. */
    private static View parseView(XMLElement x) {
        /* <view orientation=".." nodetheme=".." edgetheme=".." name=".." current="yes">
         *   <projection>
         *   <defaultnode/>
         *   <defaultedge/>
         *   0 or more <node>
         *   0 or more <edge>
         * </view>
         * The "current=yes" field is only used for choosing the default one in the parent ViewPalette object.
         */
        if (!"view".equals(x.getName())) return null;
        String name=(String)x.getAttribute("name");
        if (name==null) name="";
        View ans=new View(name);
        DotOrientation ori=parseDotOrientation(x);
        if (ori!=null) ans.getGeneralView().setGeneralOrientation(ori);
        DotPalette nodePal=parseDotPalette(x,"nodetheme");
        if (nodePal!=null) ans.getGeneralView().setNodePalette(nodePal);
        DotPalette edgePal=parseDotPalette(x,"edgetheme");
        if (edgePal!=null) ans.getGeneralView().setEdgePalette(edgePal);
        Vector subs=x.getChildren();
        for(int i=0; i<subs.size(); i++) {
            XMLElement xml=(XMLElement)(subs.get(i));
            if ("projection".equals(xml.getName())) {
                ProjectionFrame pj=parseProjectionFrame(xml);
                if (pj==null) continue;
                for(Iterator t=pj.getProjectedTypes().iterator(); t.hasNext();)
                	ans.getModelView().getProjectionFrame().projectOn((AlloyType)(t.next()));
            }
            else if ("defaultnode".equals(xml.getName())) {
                NodeViz node=parseNodeViz(xml);
                if (node!=null) ans.getGeneralView().setGeneralNodeViz(node);
            }
            else if ("defaultedge".equals(xml.getName())) {
                EdgeViz node=parseEdgeViz(xml);
                if (node!=null) ans.getGeneralView().setGeneralEdgeViz(node);
            }
            else if ("custvar".equals(xml.getName())) {
                parseCustVar(xml, ans.getModelView());
            }
            else if ("node".equals(xml.getName())) {
                NodeViz node=parseNodeViz(xml);
                if (node==null) continue;
                Vector subs2=xml.getChildren();
                for(int j=0; j<subs2.size(); j++) {
                    XMLElement entry=(XMLElement)(subs2.get(j));
                    AlloyNodeElement elt=parseAlloyType(entry);
                    if (elt instanceof AlloyType && ((AlloyType)elt).getName().equals("univ")) continue;
                    if (elt==null) elt=parseAlloySet(entry);
                    if (elt!=null) ans.getModelView().getVizMap().addNodeMapping(elt,node);
                }
            }
            else if ("edge".equals(xml.getName())) {
                EdgeViz edge=parseEdgeViz(xml);
                if (edge==null) continue;
                Vector subs2=xml.getChildren();
                for(int j=0; j<subs2.size(); j++) {
                    XMLElement entry=(XMLElement)(subs2.get(j));
                    AlloyRelation elt=parseAlloyRelation(entry);
                    if (elt!=null) ans.getModelView().getVizMap().addEdgeMapping(elt,edge);
                }
            }
        }
        return ans;
    }

    /* ******************************************************************************************* */

    /** Writes nothing if the argument is null. */
    private static void writeView(Writer out, View x, boolean isCurrent) throws IOException {
        if (x==null) return;
        //
        out.write("<view name=\"");
        encode(out, x.getName());
        if (isCurrent) out.write("\" current=\"yes");
        out.write('\"');
        writeDotOrientation(out, x.getGeneralView().getGeneralOrientation());
        writeDotPalette(out, "nodetheme", x.getGeneralView().getNodePalette());
        writeDotPalette(out, "edgetheme", x.getGeneralView().getEdgePalette());
        out.write(">\n");
        //
        writeProjectionFrame(out, x.getModelView().getProjectionFrame());
        //
        out.write("\n");
        writeNodeViz(out, x.getGeneralView().getGeneralNodeViz(), true);
        out.write(" </defaultnode>\n\n");
        writeEdgeViz(out, x.getGeneralView().getGeneralEdgeViz(), true);
        out.write(" </defaultedge>\n");
        //
        for(Iterator i=new ArrayList(x.getModelView().getModuleNames()).iterator(); i.hasNext();) {
            for(Iterator j=x.getModelView().getCustVars((String) (i.next())).iterator(); j.hasNext();) {
                writeCustVar(out, (CustVar)(j.next()));
            }
        }
        //
        final VizMap y=x.getModelView().getVizMap();
        final Map edges = new HashMap();
        final Map nodes = new HashMap();
        for(Iterator i=y.getNodes().iterator(); i.hasNext();) {
        	AlloyNodeElement a=(AlloyNodeElement)(i.next());
            NodeViz b=y.getNodeViz(a);
            List list=(List)(nodes.get(b));
            if (list==null) { list=new ArrayList(); nodes.put(b,list); }
            list.add(a);
        }
        for(Iterator i=y.getCachedNodes().iterator(); i.hasNext();) {
        	AlloyNodeElement a=(AlloyNodeElement)(i.next());
            NodeViz b=y.getCachedNodeViz(a);
            List list=(List)(nodes.get(b));
            if (list==null) { list=new ArrayList(); nodes.put(b,list); }
            list.add(a);
        }
        for(Iterator i=y.getEdges().iterator(); i.hasNext();) {
        	AlloyRelation a=(AlloyRelation)(i.next());
            EdgeViz b=y.getEdgeViz(a);
            List list=(List)(edges.get(b));
            if (list==null) { list=new ArrayList(); edges.put(b,list); }
            list.add(a);
        }
        for(Iterator i=y.getCachedEdges().iterator(); i.hasNext();) {
        	AlloyRelation a=(AlloyRelation)(i.next());
            EdgeViz b=y.getCachedEdgeViz(a);
            List list=(List)(edges.get(b));
            if (list==null) { list=new ArrayList(); edges.put(b,list); }
            list.add(a);
        }
        //
        for(Iterator i=nodes.keySet().iterator(); i.hasNext();) {
        	NodeViz node=(NodeViz)(i.next());
        	List list=(List)(nodes.get(node));
            out.write('\n');
            writeNodeViz(out, node, false);
            for(Iterator j=list.iterator(); j.hasNext();) {
            	Object b=j.next();
                out.write("\n  ");
                if (b instanceof AlloyType) writeAlloyType(out,(AlloyType)b); else writeAlloySet(out,(AlloySet)b);
            }
            out.write("\n</node>\n");
        }
        for(Iterator i=edges.keySet().iterator(); i.hasNext();) {
        	EdgeViz edge=(EdgeViz)(i.next());
        	List list=(List)(edges.get(edge));
            out.write('\n');
            writeEdgeViz(out, edge, false);
            for(Iterator j=list.iterator(); j.hasNext();) {
            	AlloyRelation b=(AlloyRelation)(j.next());
                out.write("\n   "); writeAlloyRelation(out,b);
            }
            out.write("\n</edge>\n");
        }
        //
        out.write("\n</view>\n");
    }

    /* ******************************************************************************************* */

    /** Merges the values from the XML into an existing ModelView object. */
    private static void parseCustVar(XMLElement x, ModelView mv) {
        /* class CustVar {
         *    private String _name, _expr, _moduleName;
         * }
         * <custvar name="name" expr="expr" module="module"/>
         */
        if (!"custvar".equals(x.getName())) return;
        String name=(String)x.getAttribute("name");
        String exp=(String)x.getAttribute("expr");
        String module=(String)x.getAttribute("module");
        mv.addCustVar(new CustVar(name,exp,module));
    }

    /** Writes nothing if the argument is null. */
    private static void writeCustVar(Writer out, CustVar x) throws IOException {
        out.write("\n<custvar name=\"");
        encode(out, x.getName());
        out.write("\" expr=\"");
        encode(out, x.getExpr());
        out.write("\" module=\"");
        encode(out, x.getModuleName());
        out.write("\"/>\n");
    }

    /* ******************************************************************************************* */

    /** Return null if the element is malformed. */
    private static AlloyType parseAlloyType(XMLElement x) {
        /* class AlloyType implements AlloyNodeElement {
         *      String name;
         * }
         * <type name="the type name"/>
         */
        if (!"type".equals(x.getName())) return null;
        String name=(String)x.getAttribute("name");
        if (name==null || name.length()==0) return null;
        return new AlloyType(name);
    }

    /** Writes nothing if the argument is null. */
    private static void writeAlloyType(Writer out, AlloyType x) throws IOException {
        if (x==null) return;
        out.write(" <type name=\"");
        encode(out, x.getName());
        out.write("\"/>");
    }

    /* ******************************************************************************************* */

    /** Return null if the element is malformed. */
    private static AlloySet parseAlloySet(XMLElement x) {
        /* class AlloySet implements AlloyNodeElement {
         *   String name;
         *   AlloyType type;
         *   boolean isCustom;
         * }
         * <set name="name" type="name" custom="yes"/>
         * (If custom="no", or omitted, that means no.)
         */
        if (!"set".equals(x.getName())) return null;
        String name=(String)x.getAttribute("name");
        if (name==null || name.length()==0) return null;
        String type=(String)x.getAttribute("type");
        if (type==null || type.length()==0) return null;
        AlloySet answer = new AlloySet(name, new AlloyType(type));
        Boolean custom=getbool(x, "custom", null);
        answer.setCustom(custom!=null && custom.booleanValue());
        return answer;
    }

    /** Writes nothing if the argument is null. */
    private static void writeAlloySet(Writer out, AlloySet x) throws IOException {
        if (x==null) return;
        out.write(" <set name=\"");
        encode(out, x.getName());
        out.write("\" type=\"");
        encode(out, x.getType().getName());
        if (x.isCustom()) out.write("\" custom=\"yes");
        out.write("\"/>");
    }

    /* ******************************************************************************************* */

    /** Return null if the element is malformed. */
    private static AlloyRelation parseAlloyRelation(XMLElement x) {
        /* class AlloyRelation {
         *    String _name;
         *    List<AlloyType> _types;
         *    boolean _isCustom;
         * }
         * <relation name="name" custom="yes">  // If custom="no", or is omittied, that means no
         *   1 or more <alloytype>
         * </relation>
         */
        List ans=new ArrayList();
        if (!"relation".equals(x.getName())) return null;
        String name=(String)x.getAttribute("name");
        if (name==null || name.length()==0) return null;
        Vector subs=x.getChildren();
        for(int i=0; i<subs.size(); i++) {
            AlloyType sub=parseAlloyType((XMLElement)(subs.get(i)));
            if (sub!=null) ans.add(sub); else return null;
        }
        if (ans.size()==0) return null;
        AlloyRelation rel = new AlloyRelation(name,ans);
        Boolean custom=getbool(x, "custom", null);
        rel.setCustom(custom!=null && custom.booleanValue());
        return rel;
    }

    /** Writes nothing if the argument is null. */
    private static void writeAlloyRelation(Writer out, AlloyRelation x) throws IOException {
        if (x==null) return;
        out.write("<relation name=\"");
        encode(out, x.getName());
        if (x.isCustom()) out.write("\" custom=\"yes");
        out.write("\">");
        for(Iterator i=x.getTypes().iterator(); i.hasNext();) writeAlloyType(out, (AlloyType)(i.next()));
        out.write(" </relation>");
    }

    /* ******************************************************************************************* */

    /** Always returns a nonnull (though possiblily empty) ProjectionFrame object. */
    private static ProjectionFrame parseProjectionFrame(XMLElement x) {
        /* ProjectionFrame {
        *      Set<AlloyType> _projectedTypes;
        *  }
        * =>
        * <projection>
        *   0 or more <alloytype>
        * </projection>
        */
        ProjectionFrame pf=new ProjectionFrame();
        if (!"projection".equals(x.getName())) return pf;
        Vector subs=x.getChildren();
        for(int i=0; i<subs.size(); i++) {
            AlloyType sub=parseAlloyType((XMLElement)(subs.get(i)));
            if (sub!=null) pf.projectOn(sub);
        }
        return pf;
    }

    /** Writes an empty Projection tag if the argument is null or empty */
    private static void writeProjectionFrame(Writer out, ProjectionFrame x) throws IOException {
        if (x==null) { out.write("\n<projection/>\n"); return; }
        Set types=x.getProjectedTypes();
        if (types.size()==0) { out.write("\n<projection/>\n"); return; }
        out.write("\n<projection>");
        for(Iterator i=types.iterator(); i.hasNext();) writeAlloyType(out, (AlloyType)(i.next()) );
        out.write(" </projection>\n");
    }

    /* ******************************************************************************************* */

    /** Return null if the element is malformed. */
    private static NodeViz parseNodeViz(XMLElement x) {
        /* class NodeViz {
         *     String _label;
         *     DotColor _color;
         *     DotShape _shape;
         *     DotStyle _style;
         *     Boolean _visible, _sameRank, _showLabel, _selected, _showInAttr, _hideUnconnected, _numberAtoms;
         *    }
         * <node  visible="yes/no"  label="xx"  color=".."  shape=".."  style=".."
         * samerank="yes/no"  showlabel="yes/no"   selected="yes/no"   showinattr="yes/no"
         * hideunconnected="yes/no" nubmeratoms="yes/no"/>
         * (Note: BOOLEAN is tristate. Each of the BOOLEAN ATTRIBUTE, if omitted, means INHERITED)
         * (Note: DotColor, DotShape, and DotStyle can be null (meaning INHERITED))
         */
        boolean topmost;
        if ("node".equals(x.getName())) topmost=false;
        else if ("defaultnode".equals(x.getName())) topmost=true;
        else return null;
        Boolean visible = getbool(x,"visible", (topmost?Boolean.TRUE:null));
        Boolean samerank = getbool(x,"samerank", (topmost?Boolean.FALSE:null));
        Boolean showlabel = getbool(x,"showlabel", (topmost?Boolean.TRUE:null));
        Boolean selected = getbool(x,"selected", (topmost?Boolean.TRUE:null));
        Boolean showinattr = getbool(x,"showinattr", (topmost?Boolean.FALSE:null));
        Boolean hide = getbool(x,"hideunconnected", (topmost?Boolean.FALSE:null));
        Boolean num = getbool(x,"numberatoms", (topmost?Boolean.TRUE:null));
        DotStyle style = parseDotStyle(x, (topmost?DotStyle.SOLID:null));
        DotShape shape = parseDotShape(x, (topmost?DotShape.ELLIPSE:null));
        DotColor color = parseDotColor(x, (topmost?DotColor.WHITE:null));
        String label = (String)x.getAttribute("label"); if (label==null) label="";
        return new NodeViz(visible,label,color,shape,style,samerank,showlabel,selected,showinattr,hide,num);
    }

    /** Writes nothing if the argument is null; note: this method only outputs the opening tag. */
    private static void writeNodeViz(Writer out, NodeViz x, boolean topmost) throws IOException {
        if (x==null) return;
        if (topmost) out.write("<defaultnode"); else out.write("<node");
        writeBool(out,"visible", x.isVisible());
        writeBool(out,"samerank", x.isSameRank());
        writeBool(out,"showlabel", x.showLabel());
        writeBool(out,"selected", x.isSelected());
        writeBool(out,"showinattr", x.showInAttr());
        writeBool(out,"hideunconnected", x.hideUnconnected());
        writeBool(out,"numberatoms", x.numberAtoms());
        writeDotStyle(out, x.getStyle());
        writeDotShape(out, x.getShape());
        writeDotColor(out, x.getColor());
        out.write(" label=\"");
        if (x.getLabel()!=null && x.getLabel().length()>0) encode(out, x.getLabel());
        out.write("\">");
    }

    /* ******************************************************************************************* */

    /** Return null if the element is malformed. */
    private static EdgeViz parseEdgeViz(XMLElement x) {
        /* class EdgeViz {
         *    private String _label;
         *    private DotColor _color;
         *    private DotStyle _style;
         *    private int _weight;
         *    private Boolean _visible, _attribute, _sameRank, _mergeArrows, _selected, _layoutBack;
         * }
         * <edge  visible="yes/no"  label="xx"  color=".."  style=".."  weight=".."
         * attribute="yes/no", samerank="yes/no"  merge="yes/no"  selected="yes/no"  layout="yes/no"/>
         * (Note: BOOLEAN is tristate. Each of the BOOLEAN ATTRIBUTE, if omitted, means INHERITED)
         * (Note: DotColor and DotStyle can be null (meaning INHERITED))
         */
        boolean topmost;
        if ("edge".equals(x.getName())) topmost=false;
        else if ("defaultedge".equals(x.getName())) topmost=true;
        else return null;
        Boolean visible = getbool(x,"visible", (topmost?Boolean.TRUE:null));
        Boolean attribute = getbool(x,"attribute", (topmost?Boolean.FALSE:null));
        Boolean samerank = getbool(x,"samerank", (topmost?Boolean.FALSE:null));
        Boolean merge = getbool(x,"merge", (topmost?Boolean.TRUE:null));
        Boolean selected = getbool(x,"selected", (topmost?Boolean.TRUE:null));
        Boolean layout = getbool(x,"layout", (topmost?Boolean.FALSE:null));
        int weight = getint(x,"weight"); if (weight<0) weight=0;
        DotStyle style = parseDotStyle(x, (topmost?DotStyle.SOLID:null));
        DotColor color = parseDotColor(x, (topmost?DotColor.BLACK:null));
        String label = (String)x.getAttribute("label"); if (label==null) label="";
        return new EdgeViz(visible,label,color,style,weight,attribute,samerank,merge,selected,layout);
    }

    /** Writes nothing if the argument is null; note: this method only outputs the opening tag. */
    private static void writeEdgeViz(Writer out, EdgeViz x, boolean topmost) throws IOException {
        if (x==null) return;
        if (topmost) out.write("<defaultedge"); else out.write("<edge");
        writeBool(out,"visible", x.isVisible());
        writeBool(out,"attribute", x.isAttribute());
        writeBool(out,"samerank", x.isSameRank());
        writeBool(out,"merge", x.mergeArrows());
        writeBool(out,"selected", x.isSelected());
        writeBool(out,"layout", x.layoutBack());
        writeDotColor(out, x.getColor());
        writeDotStyle(out, x.getStyle());
        out.write(" weight=\""+x.getWeight()+"\" label=\"");
        if (x.getLabel()!=null && x.getLabel().length()>0) encode(out, x.getLabel());
        out.write("\">");
    }

    /* ******************************************************************************************* */

    /** Returns VERTICAL if the attribute doesn't exist, or is malformed. */
    private static DotOrientation parseDotOrientation(XMLElement x) {
        String ans=(String)x.getAttribute("orientation");
        if ("HORIZONTAL".equalsIgnoreCase(ans)) return DotOrientation.HORIZONTAL;
        else return DotOrientation.VERTICAL;
    }

    /** Writes VERTICAL if the argument is null. */
    private static void writeDotOrientation(Writer out, DotOrientation x) throws IOException {
        if (x==DotOrientation.HORIZONTAL) out.write(" orientation=\"HORIZONTAL\"");
        else out.write(" orientation=\"VERTICAL\"");
    }

    /* ******************************************************************************************* */

    /** Returns CLASSIC if the attribute doesn't exist, or is malformed. */
    private static DotPalette parseDotPalette(XMLElement x, String key) {
        String ans=(String)x.getAttribute(key);
        if ("STANDARD".equalsIgnoreCase(ans)) return DotPalette.STANDARD;
        if ("MARTHA".equalsIgnoreCase(ans)) return DotPalette.MARTHA;
        if ("NEON".equalsIgnoreCase(ans)) return DotPalette.NEON;
        return DotPalette.CLASSIC;
    }

    /** Writes CLASSIC if the argument is null. */
    private static void writeDotPalette(Writer out, String key, DotPalette x) throws IOException {
        if (x==DotPalette.STANDARD) { out.write(" "+key+"=\"STANDARD\""); return; }
        if (x==DotPalette.MARTHA) { out.write(" "+key+"=\"MARTHA\""); return; }
        if (x==DotPalette.NEON) { out.write(" "+key+"=\"NEON\""); return; }
        out.write(" "+key+"=\"CLASSIC\"");
    }

    /* ******************************************************************************************* */

    /** Returns null if the attribute doesn't exist, or is malformed. */
    private static DotColor parseDotColor(XMLElement x, DotColor defaultValue) {
        String ans=(String)x.getAttribute("color");
        if ("WHITE".equalsIgnoreCase(ans)) return DotColor.WHITE;
        if ("GRAY".equalsIgnoreCase(ans)) return DotColor.GRAY;
        if ("BLACK".equalsIgnoreCase(ans)) return DotColor.BLACK;
        if ("STANDARD_RED".equalsIgnoreCase(ans)) return DotColor.STANDARD_RED;
        if ("STANDARD_GREEN".equalsIgnoreCase(ans)) return DotColor.STANDARD_GREEN;
        if ("STANDARD_YELLOW".equalsIgnoreCase(ans)) return DotColor.STANDARD_YELLOW;
        if ("STANDARD_BLUE".equalsIgnoreCase(ans)) return DotColor.STANDARD_BLUE;
        if ("CLASSIC_RED".equalsIgnoreCase(ans)) return DotColor.CLASSIC_RED;
        if ("CLASSIC_GREEN".equalsIgnoreCase(ans)) return DotColor.CLASSIC_GREEN;
        if ("CLASSIC_YELLOW".equalsIgnoreCase(ans)) return DotColor.CLASSIC_YELLOW;
        if ("CLASSIC_BLUE".equalsIgnoreCase(ans)) return DotColor.CLASSIC_BLUE;
        if ("MARTHA_RED".equalsIgnoreCase(ans)) return DotColor.MARTHA_RED;
        if ("MARTHA_GREEN".equalsIgnoreCase(ans)) return DotColor.MARTHA_GREEN;
        if ("MARTHA_YELLOW".equalsIgnoreCase(ans)) return DotColor.MARTHA_YELLOW;
        if ("MARTHA_BLUE".equalsIgnoreCase(ans)) return DotColor.MARTHA_BLUE;
        if ("NEON_RED".equalsIgnoreCase(ans)) return DotColor.NEON_RED;
        if ("NEON_GREEN".equalsIgnoreCase(ans)) return DotColor.NEON_GREEN;
        if ("NEON_YELLOW".equalsIgnoreCase(ans)) return DotColor.NEON_YELLOW;
        if ("NEON_BLUE".equalsIgnoreCase(ans)) return DotColor.NEON_BLUE;
        return defaultValue;
    }

    /** Writes nothing if the argument is null. */
    private static void writeDotColor(Writer out, DotColor x) throws IOException {
        if (x==DotColor.WHITE) out.write(" color=\"WHITE\"");
        if (x==DotColor.GRAY) out.write(" color=\"GRAY\"");
        if (x==DotColor.BLACK) out.write(" color=\"BLACK\"");
        if (x==DotColor.STANDARD_RED) out.write(" color=\"STANDARD_RED\"");
        if (x==DotColor.STANDARD_GREEN) out.write(" color=\"STANDARD_GREEN\"");
        if (x==DotColor.STANDARD_YELLOW) out.write(" color=\"STANDARD_YELLOW\"");
        if (x==DotColor.STANDARD_BLUE) out.write(" color=\"STANDARD_BLUE\"");
        if (x==DotColor.CLASSIC_RED) out.write(" color=\"CLASSIC_RED\"");
        if (x==DotColor.CLASSIC_GREEN) out.write(" color=\"CLASSIC_GREEN\"");
        if (x==DotColor.CLASSIC_YELLOW) out.write(" color=\"CLASSIC_YELLOW\"");
        if (x==DotColor.CLASSIC_BLUE) out.write(" color=\"CLASSIC_BLUE\"");
        if (x==DotColor.MARTHA_RED) out.write(" color=\"MARTHA_RED\"");
        if (x==DotColor.MARTHA_GREEN) out.write(" color=\"MARTHA_GREEN\"");
        if (x==DotColor.MARTHA_YELLOW) out.write(" color=\"MARTHA_YELLOW\"");
        if (x==DotColor.MARTHA_BLUE) out.write(" color=\"MARTHA_BLUE\"");
        if (x==DotColor.NEON_RED) out.write(" color=\"NEON_RED\"");
        if (x==DotColor.NEON_GREEN) out.write(" color=\"NEON_GREEN\"");
        if (x==DotColor.NEON_YELLOW) out.write(" color=\"NEON_YELLOW\"");
        if (x==DotColor.NEON_BLUE) out.write(" color=\"NEON_BLUE\"");
    }

    /* ******************************************************************************************* */

    /** Returns default if the attribute doesn't exist, or is malformed. */
    private static DotShape parseDotShape(XMLElement x, DotShape defaultValue) {
        String ans=(String)x.getAttribute("shape");
        if ("BOX".equalsIgnoreCase(ans)) return DotShape.BOX;
        if ("ELLIPSE".equalsIgnoreCase(ans)) return DotShape.ELLIPSE;
        if ("CIRCLE".equalsIgnoreCase(ans)) return DotShape.CIRCLE;
        if ("EGG".equalsIgnoreCase(ans)) return DotShape.EGG;
        if ("TRIANGLE".equalsIgnoreCase(ans)) return DotShape.TRIANGLE;
        if ("PLAIN_TEXT".equalsIgnoreCase(ans)) return DotShape.PLAIN_TEXT;
        if ("DIAMOND".equalsIgnoreCase(ans)) return DotShape.DIAMOND;
        if ("TRAPEZOID".equalsIgnoreCase(ans)) return DotShape.TRAPEZOID;
        if ("PARALLELOGRAM".equalsIgnoreCase(ans)) return DotShape.PARALLELOGRAM;
        if ("HOUSE".equalsIgnoreCase(ans)) return DotShape.HOUSE;
        if ("HEXAGON".equalsIgnoreCase(ans)) return DotShape.HEXAGON;
        if ("OCTAGON".equalsIgnoreCase(ans)) return DotShape.OCTAGON;
        if ("DOUBLE_CIRCLE".equalsIgnoreCase(ans)) return DotShape.DOUBLE_CIRCLE;
        if ("DOUBLE_OCTAGON".equalsIgnoreCase(ans)) return DotShape.DOUBLE_OCTAGON;
        if ("TRIPLE_OCTAGON".equalsIgnoreCase(ans)) return DotShape.TRIPLE_OCTAGON;
        if ("INV_TRIANGLE".equalsIgnoreCase(ans)) return DotShape.INV_TRIANGLE;
        if ("INV_TRAPEZOID".equalsIgnoreCase(ans)) return DotShape.INV_TRAPEZOID;
        if ("INV_HOUSE".equalsIgnoreCase(ans)) return DotShape.INV_HOUSE;
        if ("MDIAMOND".equalsIgnoreCase(ans)) return DotShape.MDIAMOND;
        if ("MSQUARE".equalsIgnoreCase(ans)) return DotShape.MSQUARE;
        if ("MCIRCLE".equalsIgnoreCase(ans)) return DotShape.MCIRCLE;
        return defaultValue;
    }

    /** Writes nothing if the argument is null. */
    private static void writeDotShape(Writer out, DotShape x) throws IOException {
        if (x==DotShape.BOX) out.write(" shape=\"BOX\"");
        if (x==DotShape.ELLIPSE) out.write(" shape=\"ELLIPSE\"");
        if (x==DotShape.CIRCLE) out.write(" shape=\"CIRCLE\"");
        if (x==DotShape.EGG) out.write(" shape=\"EGG\"");
        if (x==DotShape.TRIANGLE) out.write(" shape=\"TRIANGLE\"");
        if (x==DotShape.PLAIN_TEXT) out.write(" shape=\"PLAIN_TEXT\"");
        if (x==DotShape.DIAMOND) out.write(" shape=\"DIAMOND\"");
        if (x==DotShape.TRAPEZOID) out.write(" shape=\"TRAPEZOID\"");
        if (x==DotShape.PARALLELOGRAM) out.write(" shape=\"PARALLELOGRAM\"");
        if (x==DotShape.HOUSE) out.write(" shape=\"HOUSE\"");
        if (x==DotShape.HEXAGON) out.write(" shape=\"HEXAGON\"");
        if (x==DotShape.OCTAGON) out.write(" shape=\"OCTAGON\"");
        if (x==DotShape.DOUBLE_CIRCLE) out.write(" shape=\"DOUBLE_CIRCLE\"");
        if (x==DotShape.DOUBLE_OCTAGON) out.write(" shape=\"DOUBLE_OCTAGON\"");
        if (x==DotShape.TRIPLE_OCTAGON) out.write(" shape=\"TRIPLE_OCTAGON\"");
        if (x==DotShape.INV_TRIANGLE) out.write(" shape=\"INV_TRIANGLE\"");
        if (x==DotShape.INV_TRAPEZOID) out.write(" shape=\"INV_TRAPEZOID\"");
        if (x==DotShape.INV_HOUSE) out.write(" shape=\"INV_HOUSE\"");
        if (x==DotShape.MDIAMOND) out.write(" shape=\"MDIAMOND\"");
        if (x==DotShape.MSQUARE) out.write(" shape=\"MSQUARE\"");
        if (x==DotShape.MCIRCLE) out.write(" shape=\"MCIRCLE\"");
    }

    /* ******************************************************************************************* */

    /** Returns default if the attribute doesn't exist, or is malformed. */
    private static DotStyle parseDotStyle(XMLElement x, DotStyle defaultValue) {
        String ans=(String)x.getAttribute("style");
        if ("solid".equalsIgnoreCase(ans)) return DotStyle.SOLID;
        if ("dashed".equalsIgnoreCase(ans)) return DotStyle.DASHED;
        if ("dotted".equalsIgnoreCase(ans)) return DotStyle.DOTTED;
        if ("bold".equalsIgnoreCase(ans)) return DotStyle.BOLD;
        if ("invis".equalsIgnoreCase(ans)) return DotStyle.INVIS;
        return defaultValue;
    }

    /** Writes nothing if the argument is null. */
    private static void writeDotStyle(Writer out, DotStyle x) throws IOException {
        if (x==DotStyle.SOLID) out.write(" style=\"solid\"");
        if (x==DotStyle.DASHED) out.write(" style=\"dashed\"");
        if (x==DotStyle.DOTTED) out.write(" style=\"dotted\"");
        if (x==DotStyle.BOLD) out.write(" style=\"bold\"");
        if (x==DotStyle.INVIS) out.write(" style=\"invis\"");
    }

    /* ******************************************************************************************* */

    /** Returns null if the attribute doesn't exist, or is malformed. */
    private static Boolean getbool(XMLElement x, String attr, Boolean defaultValue) {
        String value=(String)x.getAttribute(attr);
        if (value==null) return defaultValue;
        if (value.equalsIgnoreCase("yes") || value.equalsIgnoreCase("true")) return Boolean.valueOf(true);
        if (value.equalsIgnoreCase("no") || value.equalsIgnoreCase("false")) return Boolean.valueOf(false);
        return defaultValue;
    }

    /** Writes nothing if the argument is null. */
    private static void writeBool(Writer out, String key, Boolean value) throws IOException {
        if (value==null) return;
        out.write(' ');
        out.write(key);
        if (value.booleanValue()) out.write("=\"yes\""); else out.write("=\"no\"");
    }

    /** Always writes something. */
    private static void writeBool(Writer out, String key, boolean value) throws IOException {
        out.write(' ');
        out.write(key);
        if (value) out.write("=\"yes\""); else out.write("=\"no\"");
    }

    /* ******************************************************************************************* */

    /** Returns 0 if the attribute doesn't exist, or is malformed. */
    private static int getint(XMLElement x, String attr) {
        String value=(String)x.getAttribute(attr);
        if (value==null) return 0;
        int i;
        try {
            i=Integer.parseInt(value);
        } catch(NumberFormatException ex) {
            i=0;
        }
        return i;
    }

    /* ******************************************************************************************* */

    private static void encode(Writer out, String str) throws IOException {
        int n=str.length();
        for(int i=0; i<n; i++) {
            char c=str.charAt(i);
            if (c=='<') { out.write("&lt;"); continue; }
            if (c=='>') { out.write("&gt;"); continue; }
            if (c=='&') { out.write("&amp;"); continue; }
            if (c=='\'') { out.write("&apos;"); continue; }
            if (c=='\"') { out.write("&quot;"); continue; }
            if (c>=32 && c<127) { out.write(c); continue; }
            out.write("&#x");
            out.write(Integer.toString((int)c, 16));
            out.write(';');
        }
    }
}
